[RUBY] [Rails] A memo that created an advanced search form with ransack

Introduction

It is a description of creating the following advanced search form of Mercari. It is not a summary, but a detailed notice.

I hope it will serve as a reference for those who have encountered similar issues.

56b5aab1bc98bb048821fc50814e8eeb.gif

Completed form

▼ Sort 201418e4d94c98c62f249943c2713b93.gif

▼ Category f509c4b3dbb2abf3fad585bebb2963c0.gif

▼ Price range, check box d9de65f9cc351577645bcb4aee008bf9.gif

▼ Clear button d6353bb4e2e151c601fe06ab613e79ae.gif

Clogged part

  1. ransack query problem
  2. Implementation of sorting search results
  3. Representation in select box view
  4. Representation of checkboxes in view
  5. Relationship between items that use ancestry and ransack
  6. Implementation of "All" button in JavaScript
  7. Implementation of clear button for search conditions

Preface

Please refer to Github and the following articles for explanation and usage of ransack.

Github - ransack Summary of various search form creation methods using [Rails] ransack

1. ransack query problem

1-1. Challenge: Search query (@q) needs to be placed on each controller

As shown in the above figure, since the search form is placed in the header, it is necessary to set the query (@q) in before_action on most controllers.

Otherwise, the following error (No Ransack :: Search object was provided to search_form_for!) Will occur. スクリーンショット 2020-06-09 18.12.03.png

It is necessary to describe the index action in Qiita mentioned above.

items_controller.rb


class ItemsController < ApplicationController
  def index
    @q = Item.ransack(params[:q])
    #Defined here@Without q, search_form_The above error occurs in for
    @items = @q.result(distinct: true)
  end

  def search
    @q = Item.search(search_params)
    @items = @q.result(distinct: true)
  end

  private
  def search_params
    params.require(:q).permit!
  end
end

Initially, each controller defined a private method and did before_action.

tops_controller.rb


class TopsController < ApplicationController
  skip_before_action :authenticate_user!
  before_action :set_item_search_query

  def index  
    @items = Item.including.limit(5).desc.trading_not
  end
  
  private
  def set_item_search_query
    @q = Student.ransack(params[:q])
    @items = @q.result(distinct: true)
  end

end

However, with this method, it is necessary to define a private method of set_item_search_query in all the controllers that call ** the page that has the search form (= display the header) **, which violates the DRY principle **. (The description is troublesome above all).

1-2. Countermeasures

In conclusion, I defined a method in ʻapplication_controller.rb` and called it on each required controller.

application_controller.rb


class ApplicationController < ActionController::Base
~~
  def set_item_search_query
    @q = Item.ransack(params[:q])
    @items = @q.result(distinct: true)
  end
~~
end

tops_controller.rb


class TopsController < ApplicationController
  skip_before_action :authenticate_user!
  before_action :set_item_search_query  #Add this description

  def index  
    @items = Item.including.limit(5).desc.trading_not
  end
  
  #The following description is deleted
  # private
  # def set_item_search_query
  #   @q = Student.ransack(params[:q])
  #   @items = @q.result(distinct: true)
  # end

end

1-3. Remaining tasks (rather, lack of understanding)

The set_item_search_query method could be called from another controller regardless of whether it was defined above or below private in ʻapplication_controller.rb`.

I'm wondering what this means because private is originally meant to define methods that I don't want to be called by other controllers.

2. Implementation of sorting search results

2-1. Challenge: How to achieve sorting in descending order of related child models

I had a hard time sorting in the order of the most "favorites!" Associated with Items (the order of the most related child models). Other than that, the following sorting was relatively easy to implement because it simply sorts the columns of the Item model by order.

--Lowest price --In descending order of price --New order of listing --Oldest order of listing

The view and controller are as follows. Simply put, it fires an event when the pull-down is changed in js and passes the selected value to the controller as a sort parameter.

Each code (click to open)

ruby:search.html.haml


.sc-side__sort
  %select{name: :sort_order, class: 'sort-order .sc-side__sort__select'}
    %option{value: "location.pathname", name: "location.pathname"}
sort
    %option{value: "price-asc"}
Price in ascending order
    %option{value: "price-desc"}
Highest price
    %option{value: "created_at-asc"}
Listings oldest
    %option{value: "created_at-desc"}
Newest order of listing
    %option{value: "likes-desc"}
favorite!In descending order

items_controller.rb


class ItemsController < ApplicationController
  skip_before_action :authenticate_user!, except: [:new, :edit, :create, :update, :destroy]
  before_action :set_item_search_query, expect: [:search]
~~
  def search
    sort = params[:sort] || "created_at DESC"      
    @q = Item.includes(:images).(search_params)
    @items = @q.result(distinct: true).order(sort)
  end
~~
end

item_search.js


//Sorting behavior
$(function() {
  //Event occurs by selecting the pull-down menu
  $('select[name=sort_order]').change(function() {
    //Get the value attribute of the selected option tag
    const sort_order = $(this).val();
    //Branch of page transition destination depending on the value of value attribute

    switch (sort_order) {
      case 'price-asc': html = "&sort=price+asc"; break;
      case 'price-desc': html = "&sort=price+desc"; break;
      case 'created_at-asc': html = "&sort=created_at+asc"; break;
      case 'created_at-desc': html = "&sort=created_at+desc"; break;
      case 'likes-desc': html = "&sort=likes_count_desc"; break;
      default: html = "&sort=created_at+desc"; 
    }
    //Current display page
    let current_html = window.location.href;
    //Duplicate sorting function
    if (location['href'].match(/&sort=*.+/) != null) {
      var remove = location['href'].match(/&sort=*.+/)[0]
      current_html = current_html.replace(remove, '')
    };
    //Page transition
    window.location.href = current_html + html
  });
  //Behavior after page transition
  $(function () {
    if (location['href'].match(/&sort=*.+/) != null) {
      // option[selected: 'selected']Delete
      if ($('select option[selected=selected]')) {
        $('select option:first').prop('selected', false);
      }

      const selected_option = location['href'].match(/&sort=*.+/)[0].replace('&sort=', '');

      switch (selected_option) {
        case "price+asc": var sort = 1; break;
        case "price+desc": var sort = 2; break;
        case "created_at+asc": var sort = 3; break;
        case "created_at+desc": var sort = 4; break;
        case "likes_count_desc": var sort = 5; break;
        default: var sort = 0
      }
      const add_selected = $('select[name=sort_order]').children()[sort]
      $(add_selected).attr('selected', true)
    }
  });
});

2-2. Countermeasures

I thought that I couldn't pass the code that expresses the order of the most child models to the parameters passed to the controller, so I decided to make a conditional branch on the controller side.

items_controller.rb


class ItemsController < ApplicationController
  ~~
  def search
    @q = Item.includes(:images).search(search_params)
    sort = params[:sort] || "created_at DESC"    
    #The parameter that flew from js"likes_count_desc"In the case of, the description to sort in descending order of child models
    if sort == "likes_count_desc"
      @items = @q.result(distinct: true).select('items.*', 'count(likes.id) AS likes')
        .left_joins(:likes)
        .group('items.id')
        .order('likes DESC').order('created_at DESC')
    else
      @items = @q.result(distinct: true).order(sort)
    end
  end
  ~~
end

2-3. Remaining tasks

There seems to be a better description ... I would like to know who understands it.

3. Representation in select box view

3-1. Challenge: How to realize options that have a different axis from the model properties, such as "price range"?

53b473176e29ae82957ead2f8d843b37.gif

↑ Like this, I want to realize the function that the lower limit and the upper limit enter the columns respectively when the price range is selected. However, the property that Item has is price (: integer type), so it seems that it cannot be used.

3-2. Countermeasures

Create something that can handle hash data in the same way as a model (ActiveRecord) with ʻactive_hash`.

I didn't think "use active_hash!" From the beginning,

-Require an array that can be passed to the helper method of ʻoptions_from_collection_for_select` --Moreover, it is necessary to change the values passed to min and max according to the selected content. --Troublesome to write this in js ――Can you use the model? But it's a hassle to make a model and even a table

I thought that I used active_hash in the elimination method.

Click here for articles that refer to active_hash ⇒ active_hash summary

Specifically, I created a model called price_range (which can be treated as) and displayed it in the view.

** ① Create a file manually in app / models and fill in the following **

price_range.rb


class PriceRange < ActiveHash::Base
  self.data = [
      {id: 1, name: '¥300〜¥1,000', min: 300, max: 1000},
      {id: 2, name: '¥1,000〜¥5,000', min: 1000, max: 5000},
      {id: 3, name: '¥5,000〜¥10,000', min: 5000, max: 10000},
      {id: 4, name: '¥10,000〜¥30,000', min: 10000, max: 30000},
      {id: 5, name: '¥30,000〜¥50,000', min: 30000, max: 50000},
  ]
end
  • If it is the original active_hash, it will be linked with the Item model, but since it is not necessary to link the price_range with the Item model this time, the description of belongs_to_active_hash is omitted.

** ② Create array data with view file and display with ʻoptions_from_collection_for_select` **

ruby:search.html.haml


~~
#Price search criteria
.sc-side__detail__field
  %h5.sc-side__detail__field__label
    %i.fas.fa-search-dollar
    = f.label :price, 'price'
  .sc-side__detail__field__form
    # PriceRange.all, active_Data defined by hash(hash)As an array
    #It, options_from_collection_for_Expand with select helper method and pull down
    = select_tag :price_range, options_from_collection_for_select(PriceRange.all, :id, :name), { prompt: "Please select"}
  .sc-side__detail__field__form.price-range
    = f.number_field :price_gteq, placeholder: "¥ Min", class: 'price-range__input'
    .price-range__swang 〜
    = f.number_field :price_lteq, placeholder: "¥ Max", class: 'price-range__input'
~~

3-3. Remaining tasks

I feel that it is not the original usage of active_hash. .. ..

4. Representation of checkboxes in view

4-1. Challenge: How to express options without a model

Among the properties of the Item model (product), the options are as follows. Most of the options are active_hash to create a model (what can be treated as).

① Product status (status_id) → Model with active_hash? Create ② Burden of shipping charges (delivery_charge_flag) → Describe options directly in the view ③ Sales status (trading_status_id) → Model with active_hash? Create ④Category (category_id) → categories There is a table

Everything except ① was in trouble.

4-1-1. ① Product status (status_id) → Model with active_hash? Create

Before the details of the assignment, click here for the description of ①. (Click to open)

status.rb


# active_is hash
class Status < ActiveHash::Base
  self.data = [
      {id: 1, name: 'New, unused'},
      {id: 2, name: 'Nearly unused'},
      {id: 3, name: 'No noticeable scratches or stains'},
      {id: 4, name: 'Slightly scratched and dirty'},
      {id: 5, name: 'There are scratches and dirt'},
      {id: 6, name: 'Overall poor condition'},
  ]
end

item.rb


class Item < ApplicationRecord
~~
  belongs_to_active_hash :status
~~

ruby:search.html.haml


~~
#Search conditions for product status
.sc-side__detail__field
  %h5.sc-side__detail__field__label
    %i.fas.fa-star
    = f.label :status_id_eq, 'Product condition'
  .sc-side__detail__field__form.checkbox-list
    .sc-side__detail__field__form--checkbox.js_search_checkbox-all
      .sc-side__detail__field__form--checkbox__btn
        %input{type: 'checkbox', id: 'status_all', class: 'js-checkbox-all'}
      .sc-side__detail__field__form--checkbox__label
        = label_tag :status_all, 'all'
    = f.collection_check_boxes :status_id_in, Status.all, :id, :name, include_hidden: false do |b|
      .sc-side__detail__field__form--checkbox
        .sc-side__detail__field__form--checkbox__btn.js_search_checkbox
          = b.check_box
        .sc-side__detail__field__form--checkbox__label
          = b.label { b.text}
~~

With this description, you can create a group of check boxes including "all". スクリーンショット 2020-06-09 21.25.33.png

With this description, the check boxes are displayed as they are, and the check boxes are displayed on the screen after the selection and search. (Because the search condition is held as a parameter in the @ q instance and jumps to the controller, and the @ q returned after the search contains that parameter)

4-1-2. ② Burden of shipping charges (delivery_charge_flag) → Describe options directly in the view

The troublesome factor is that in the form at the time of product listing (item / new), the burden of shipping fee (whether it is the seller's burden or the purchaser's burden) is simply writing the option in the view.

ruby:views/items/_form.html.haml


~~
.field__form.item-area__field__form
  = f.select :delivery_charge_flag, [['postage included(Exhibitor burden)', '1'], ['Cash on delivery(Buyer burden)', '2']], include_blank: 'Please select', required: :true
~~

Moreover, the js that changes the delivery method is described separately according to the flag (1 or 2), so if you change it significantly, the range of influence will be large and an unexpected error will occur.

In other words, it seems that you cannot use active_hash in the same way as ** ① **.

4-1-3. ③ Sales status (trading_status_id) → Model with active_hash? Create

This was unexpectedly troublesome.

Initially, trading_status was designed below.

trading_status.rb


class TradingStatus < ActiveHash::Base
  self.data = [
      {id: 1, name: 'Sale'},
      {id: 2, name: 'During trading'},
      {id: 3, name: 'Sold'},
      {id: 4, name: 'draft'}
  ]
end

As far as I can see, it seems simple that "on sale" has an id of 1 and "sold out" has an id of 3, but that is not the case. "Trading (id: 2)" is also included in ** "Sold out" **. (Not for sale)

In other words, ** the array obtained by active_hash cannot be used as a search condition as it is **.

When I tried to change this status, I didn't have time to fix it without leaking it because the influence was wide here as well. (The first design should have been better)

  • ④ The category will be described later.

4-2. Countermeasures

Correspondence to ②

This was surprisingly simple. When defining data with active_hash, if you use flag instead of ʻid`, you can use active_hash in the same way as ①.

delivery_charge.rb


class DeliveryCharge < ActiveHash::Base
  self.data = [
      {flag: 1, name: 'postage included(Exhibitor burden)'},
      {flag: 2, name: 'Cash on delivery(Buyer burden)'}
  ]
end

Correspondence to ③

This is the feeling of pushing with the controller. So it's really hard to understand.

If you itemize what you did

--On the search condition screen, create an array (@trading_status) to display the choices as check boxes → This is just an instance of ** to display the search condition --Once you perform a normal search for ransack (at this time, "sales status" is not a search condition) ... ʻA --Branch the processing to be performed when "Sales status" is specified as a search condition (whenparams.require (: q) [: trading_status_id_in]is true) --Redefine@ qbecause it is necessary to return the" sales status "specified as the search condition on the screen after the search. --Define and use private methods for redefinition --Processing is branched when the number of "sales status" specified as the search condition is one → When there are two, both "on sale" and "sold out" are specified = "sales status" is the search condition Will not be --When "Sold out" is specified, buyer_id gets items of not nil ...B --Extract only the items that are common to ʻA and B and define them in @ items --In other words, the @items searched by the search conditions other than" sales status "is compared with the sold_items which is "sold out", and only the matching items are acquired. ――Do the same thing for "on sale" (= buyer_id is nil)

items_controller.rb


~~
def search
  #Define an instance to be an array to display the search condition of "Sales Status"
  #id 1 and 3=On sale, sold
  @trading_status = TradingStatus.find [1,3]

  #Once, perform a normal search for ransack (at this time, "sales status" is not a search condition)
  sort = params[:sort] || "created_at DESC"      
  @q = Item.not_draft.search(search_params)
  @items = @q.result(distinct: true).order(sort)
~~
  #When the sales status is in the search conditions
  if trading_status_key = params.require(:q)[:trading_status_id_in]
    #Return to the screen after searching@Redefinition of q
    @q = Item.includes(:images).search(search_params_for_trading_status)
    #One specified sales condition (If two are specified, both on sale and sold out are specified = not specified)
    #And processing when the specified key is 3 = "sold out"
    if trading_status_key.count == 1 && trading_status_key == ["3"]
      #Get sold out items → In my team, buyer in the items table_It was defined that id has a value = sold out
      sold_items = Item.where.not(buyer_id: nil)
      #upper@items were searched by the search conditions received by ransack@items
      #And sold_Extracting matching items with items → That is&
      @items = @items & sold_items
    elsif trading_status_key.count == 1 && trading_status_key == ["1"]
      #Get Items for sale → In my team, buyer in the items table_It was defined that id is nil = on sale
      selling_items = Item.where(buyer_id: nil)
      @items = @items & selling_items
    end
  end

  private
  #When "Sales Status" is not specified as a search condition@q → ransack is searching with this search condition
  def search_params
    params.require(:q).permit(
      :name_or_explanation_cont,
      :category_id,
      :price_gteq,
      :price_lteq,
      category_id_in: [],
      status_id_in: [],
      delivery_charge_flag_in: [],
    )
  end

  #When "Sales Status" is specified as a search condition@q
  def search_params_for_trading_status
    params.require(:q).permit(
      :name_or_explanation_cont,
      :category_id,
      :price_gteq,
      :price_lteq,
      category_id_in: [],
      status_id_in: [],
      delivery_charge_flag_in: [],
      trading_status_id_in: [],
    )
  end
end
~~

4-3. Remaining tasks

The part that has become quite complicated. It was a scene that made me realize that the first design was important. I want to find a better way than this!

Serious? The remaining issues are

--@items is not[] --@items = @items & selling_items or @items = @items & sold_items returns [] (= that is, there is no corresponding item)

Under the condition, the bullet reacts.

スクリーンショット 2020-06-09 23.30.11.png

ʻAVOID (Avoid!), So it seems that the N + 1 problem has not actually occurred, but ... When I chewed the English sentence, I wondered if it was "avoid it because eager loading was detected where it was not needed". However, if you remove .includes (: images) as specified, you will get an alert of ʻUSE (use!) For other patterns as you might expect.

I gave up once because the conditional branch did not work here.

5. Relationship between items that use ancestry and ransack

5-1. Challenge: How to send a category to the controller as a search condition

The categories are divided into parents, children and grandchildren. The items table has only category_id, and the category_id selected here is the id of the grandchild category.

On the other hand, as a search method, it is overwhelmingly convenient to be able to search by "when only the parent category is selected" or "when even the child category is selected". How to achieve this? Is an issue.

If you subdivide the issues further

① I want to make the grandchild category a check box instead of pull-down (2) The conditions of "when only the parent category is selected" and "when the child category is selected" cannot be returned properly in the search condition field after the search. ③ If you use a form that receives only category_id_in, you cannot search by parent category / child category. ④ If you submit the form with only the parent category selected, the parameters will be skipped with category_id blank.

Many ...

5-1-1. ① I want to make the grandchild category a check box instead of pull-down.

Only one grandchild category can be selected (pull-down), not ↓ c2a4191736e7b580941b142cb934587d.gif

I want to be able to select multiple grandchild categories (check box) ↓ d97b6e77fc3888c783c49f51b73c1304.gif

5-1-2. (2) The conditions of "when only the parent category is selected" and "when the child category is selected" cannot be returned to the search condition field after the search.

For example, if you specify and search like this, スクリーンショット 2020-06-09 23.54.22.png ↓ After the search, the condition field will be in a state where nothing is specified (the same applies when even child categories are selected). スクリーンショット 2020-06-09 23.55.18.png

5-1-3. ③ If you use a form that receives only category_id_in, you cannot search by parent category / child category.

The description of the first view was like this, but in this case the category did not work as a search condition unless the grandchild category was selected.

ruby:search.html.haml


%li
  = f.select :category_id_in ,options_for_select(@parents, @item.category.root.name),{include_blank: "Please select"}, {id: 'parent_category', required: :true}
%li
  = f.select :category_id_in ,options_for_select(@category_child_array.map{|c|[c[:name], c[:id]]}, @item.category.parent.id),{include_blank: "Please select"}, {id: 'children_category', required: :true}
%li
  = f.select :category_id_in ,options_for_select(@category_grandchild_array.map{|c|[c[:name], c[:id]]}, @item.category.id),{include_blank: "Please select"}, {id: 'grandchildren_category', required: :true}

Only the grandchild category can be searched by the category_id_in received by the parameter, so even if only the parent category is selected in this state, there is no corresponding product. (Even if you select only "Men's", you cannot search for products whose parent category is Men's.)

5-1-4. ④ If you submit the form with only the parent category selected, the parameters will be skipped with the category_id blank.

As a countermeasure for (3), the properties of the parent category and child category were set to category_id (the reason will be described later), but if the search is executed with the child category not selected, category_id will be skipped in a blank state.

5-2. Countermeasures

It is a push with js and the controller.

5-2-1. (1) Correspondence to want to make the grandchild category a check box instead of pull-down

Resolve the view and js as follows.

Click to view code

ruby:search.html.haml


.sc-side__detail__field
  %h5.sc-side__detail__field__label
    %i.fas.fa-list-ul
    = f.label :category_id, 'Select a category'
  .sc-side__detail__field__form
    %ul.field__input--category_search
      - if @search_category.present?
        %li
          = f.select :category_id, options_for_select(@search_parents, @search_category.root.name),{include_blank: "Please select"}, {id: 'parent_category_search'}
        %li
          - if @category_child.present?
            = f.select :category_id, options_for_select(@category_child_array, @category_child.id),{include_blank: "Please select"}, {id: 'children_category_search'}
          - else
            = f.select :category_id, @category_child_array, {include_blank: "Please select"}, {id: 'children_category_search'}
        - if @category_grandchild_array.present?
          %li#grandchildren_category_checkboxes.checkbox-list
            .sc-side__detail__field__form--checkbox.js_search_checkbox-all
              .sc-side__detail__field__form--checkbox__btn
                %input{type: 'checkbox', id: 'grandchildren_category_all', class: 'js-checkbox-all'}
              .sc-side__detail__field__form--checkbox__label
                = label_tag :grandchildren_category_all, 'all'
            = f.collection_check_boxes :category_id_in, @category_grandchild_array, :id, :name, include_hidden: false do |b|
              .sc-side__detail__field__form--checkbox
                .sc-side__detail__field__form--checkbox__btn.js_search_checkbox
                  = b.check_box
                .sc-side__detail__field__form--checkbox__label
                  = b.label { b.text}
      - else
        %li
          = f.select :category_id, @search_parents, {include_blank: "Please select"}, {id: 'parent_category_search'}

item_search.js


  //Category behavior in search form
  function appendOption(category){
    let html = `<option value="${category.id}" >${category.name}</option>`;
    return html;
  }

  function appendCheckbox(category){
    let html =`
                <div class="sc-side__detail__field__form--checkbox">
                  <div class="sc-side__detail__field__form--checkbox__btn js_search_checkbox">
                    <input type="checkbox" value="${category.id}" name="q[category_id_in][]" id="q_category_id_in_${category.id}" >
                  </div>
                  <div class="sc-side__detail__field__form--checkbox__label">
                    <label for="q_category_id_in_${category.id}">${category.name}</label>
                  </div>
                </div>
                `
    return html;
  }

  //Create display of child categories
  function appendChildrenBox(insertHTML){
    const childSelectHtml = `<li>
                              <select id="children_category_search" name="q[category_id]">
                                <option value="">Please select</option>
                                ${insertHTML}
                              </select>
                            </li>`;
    $('.field__input--category_search').append(childSelectHtml);
  }
  //Create display of grandchild category
  function appendGrandchildrenBox(insertHTML){
    const grandchildSelectHtml =`
                                <li id="grandchildren_category_checkboxes" class="checkbox-list">
                                  <div class="sc-side__detail__field__form--checkbox js_search_checkbox-all">
                                    <div class="sc-side__detail__field__form--checkbox__btn">
                                      <input class="js-checkbox-all" id="grandchildren_category_all" type="checkbox">
                                    </div>
                                    <div class="sc-side__detail__field__form--checkbox__label">
                                      <label for="grandchildren_category_all">all</label>
                                    </div>
                                  </div>
                                  ${insertHTML}
                                </li>`;
    $('.field__input--category_search').append(grandchildSelectHtml);
  }
  //Events after selecting the parent category
  $('#parent_category_search').on('change', function(){
    //Get the name of the selected parent category
    const parentName =$(this).val(); 
    if (parentName != ""){ 
      //Make sure the parent category is not the default
      $.ajax({
        url: '/items/category_children',
        type: 'GET',
        data: { parent_name: parentName },
        dataType: 'json'
      })
      .done(function(children){
         //When the parent changes, delete the child and below
        $('#children_category_search').remove();
        $('#grandchildren_category_checkboxes').remove();
        let insertHTML = '';
        children.forEach(function(child){
          insertHTML += appendOption(child);
        });
        appendChildrenBox(insertHTML);
      })
      .fail(function(){
        alert('Failed to get the category');
      })
    }else{
      //When the parent category becomes the initial value, delete the child and below
      $('#children_category_search').remove();
      $('#grandchildren_category_checkboxes').remove();
    }
  });
  //Event after selecting child category
  $('.field__input--category_search').on('change', '#children_category_search', function(){
    const childId = $(this).val();
    //Get id of selected child category
    if (childId != ""){ 
      //Make sure the child category is not the default
      $.ajax({
        url: '/items/category_grandchildren',
        type: 'GET',
        data: { child_id:  childId},
        dataType: 'json'
      })
      .done(function(grandchildren){
        if (grandchildren.length != 0) {
          //When a child changes, delete the grandchildren and below
          $('#grandchildren_category_checkboxes').remove();
          let insertHTML = '';
          grandchildren.forEach(function(grandchild){
            insertHTML += appendCheckbox(grandchild);
          });
          appendGrandchildrenBox(insertHTML);
        }
      })
      .fail(function(){
        alert('Failed to get the category');
      })
    }else{
      $('#grandchildren_category_checkboxes').remove();
    }
  });

5-2-2. Correspondence to ②③

Set the property name of the form of the parent category and child category to category_id and process it with the controller. The view is above.

Click here for controller

items_controller.rb


  def search
    @trading_status = TradingStatus.find [1,3]
    @keyword = params.require(:q)[:name_or_explanation_cont]
    @search_parents = Category.where(ancestry: nil).where.not(name: "Category list").pluck(:name)

    sort = params[:sort] || "created_at DESC"      
    @q = Item.not_draft.search(search_params)
    if sort == "likes_count_desc"
      @items = @q.result(distinct: true).select('items.*', 'count(likes.id) AS likes')
        .left_joins(:likes)
        .group('items.id')
        .order('likes DESC')
        .desc
    else
      @items = @q.result(distinct: true).order(sort)
    end
    #When the sales status is in the search conditions
    if trading_status_key = params.require(:q)[:trading_status_id_in]
      @q = Item.including.search(search_params_for_trading_status)
      if trading_status_key.count == 1 && trading_status_key == ["3"]
        sold_items = Item.where.not(buyer_id: nil)
        @items = @items & sold_items
      elsif trading_status_key.count == 1 && trading_status_key == ["1"]
        selling_items = Item.where(buyer_id: nil)
        @items = @items & selling_items
      end
    end

    #When the category is in the search criteria
    if category_key = params.require(:q)[:category_id]
      if category_key.to_i == 0
        @search_category = Category.find_by(name: category_key, ancestry: nil)
      else
        @search_category = Category.find(category_key)
      end

      if @search_category.present?
        if @search_category.ancestry.nil?
          #Parent category
          @category_child_array = Category.where(ancestry: @search_category.id).pluck(:name, :id)
          grandchildren_id = @search_category.indirect_ids.sort
          find_category_item(grandchildren_id)
        elsif @search_category.ancestry.exclude?("/")
          #Child category
          @category_child = @search_category
          @category_child_array = @search_category.siblings.pluck(:name, :id)
          @category_grandchild_array = @search_category.children
          grandchildren_id = @search_category.child_ids
          find_category_item(grandchildren_id)
        end
          #Pick up grandchild categories with ransack → category_id_in
      end
    end
    @items = Kaminari.paginate_array(@items).page(params[:page]).per(20)
  end

5-2-3. ④ When submitting a form with only the parent category selected, the parameter skips when category_id is blank.

In js, when the child category is blank (unselected) when executing the search, it corresponds to the process of removing the pull-down of the child category

item_search.js


  $('#detail_search_btn').click(function(e) {
    if ($('#children_category_search').val() == "") {
      $('#children_category_search').remove();
    }
  });

5-3. Remaining tasks

Like 4-3, the bullet sometimes reacts ...

Also, if you execute a search (= submit a form) with only the parent category selected (the child category is not selected), the category_id parameter will be passed to the controller blank. This time I took the method of deleting the child category f.select with js, but there seems to be a better way.

6. Implementation of "All" button in JavaScript

6-1. Challenge: Realization of unexpectedly complicated "everything"

--When you press "All", select all the corresponding options. ――If even one of the applicable options is unchecked, remove "All" as well. --Check "All" when all options other than "All" are selected. --On the screen after searching, if all options other than "All" are selected, check "All" as well.

I was addicted to how to realize the unexpectedly complicated requirement. ↓ Behavior like this 836dea5b6925b9d31ed861810c3352d6.gif

6-2. Countermeasures

Implemented with js.

There are three main types of processing. ** ① Behavior when "All" is clicked ** ** ② Behavior when clicking other than "All" ** ** ③ Function to judge whether to check the check box "All" when loading the page **

If you do not write the process of ③, execute the search with all the options selected in the search conditions → The "All" check box will be unchecked from the condition column after the search, which is awkward.

Click here for the

code. </ summary>

item_search.js


$(function(){
  const min_price = $('#q_price_gteq');
  const max_price = $('#q_price_lteq');
  let grandchild_category_all_checkbox = $('#grandchildren_category_all');
  let grandchild_category_checkboxes = $('input[name="q[category_id_in][]"]');
  const status_all_checkbox = $('#status_all');
  const status_checkboxes = $('input[name="q[status_id_in][]"]')
  const delivery_charge_all_checkbox = $('#delivery_charge_flag_all')
  const delivery_charge_checkboxes = $('input[name="q[delivery_charge_flag_in][]"]')
  const trading_status_all_checkbox = $('#trading_status_all')
  const trading_status_checkboxes = $('input[name="q[trading_status_id_in][]"]')

  //① Behavior when clicking "All"
  $(document).on('change', '.js-checkbox-all', function() {
    function targetCheckboxesChage(target, trigger) {
      if (trigger.prop("checked") == true) {
        target.prop("checked", true);
      } else {
        target.prop("checked", false);
      }
    }

    let target_checkboxes;
    switch ($(this).prop('id')) {
      case $('#grandchildren_category_all').prop('id'):
        target_checkboxes = $('input[name="q[category_id_in][]"]');
        break;
      case status_all_checkbox.prop('id'):
        target_checkboxes = status_checkboxes;
        break;
      case delivery_charge_all_checkbox.prop('id'):
        target_checkboxes = delivery_charge_checkboxes;
        break;
      case trading_status_all_checkbox.prop('id'):
        target_checkboxes = trading_status_checkboxes;
        break;
      default: ;
    }
    targetCheckboxesChage(target_checkboxes, $(this));
  });

  //② Behavior when clicking other than "All"
  $(document).on('change', '.js_search_checkbox > input:checkbox', function() {
    function allCheckboxChange(target, all_checkbox, trigger) {
      if (trigger.prop("checked") == false) {
        all_checkbox.prop("checked", false);
      } else {
        let flag = true
        target.each(function(e) {
          if (target.eq(e).prop("checked") == false) {
            flag = false;
          }
        });
        if (flag) {
          all_checkbox.prop("checked", true);
        }
      }
    }  
    let all_checkbox;
    grandchild_category_all_checkbox = $('#grandchildren_category_all');
    grandchild_category_checkboxes = $('input[name="q[category_id_in][]"]');
    switch ($(this).prop('name')) {
      case grandchild_category_checkboxes.prop('name'):
        target_checkboxes = grandchild_category_checkboxes;
        all_checkbox = grandchild_category_all_checkbox;
        break;
      case status_checkboxes.prop('name'):
        target_checkboxes = status_checkboxes;
        all_checkbox = status_all_checkbox;
        break;
      case delivery_charge_checkboxes.prop('name'):
        target_checkboxes = delivery_charge_checkboxes;
        all_checkbox = delivery_charge_all_checkbox;
        break;
      case trading_status_checkboxes.prop('name'):
        target_checkboxes = trading_status_checkboxes;
        all_checkbox = trading_status_all_checkbox;
        break;
      default: ;
    }
    allCheckboxChange(target_checkboxes, all_checkbox, $(this));
  });


  //Function that determines whether to check the check box "All" when loading a page
  function loadCheckboxSlection(target, all_checkbox) {
    let flag = true;
    target.each(function(e) {
      if (target.eq(e).prop("checked") == false) {
        flag = false;
      }
    });
    if (flag) {
      all_checkbox.prop("checked", true);
    }
  }

  //③ When the page is loaded, run a function that determines whether to check the check box "All".
  if ($('#item_search_form').length) {
    loadCheckboxSlection(grandchild_category_checkboxes ,grandchild_category_all_checkbox)
    loadCheckboxSlection(status_checkboxes, status_all_checkbox)
    loadCheckboxSlection(delivery_charge_checkboxes, delivery_charge_all_checkbox)
    loadCheckboxSlection(trading_status_checkboxes, trading_status_all_checkbox)

    if (min_price.val() != "" && max_price.val() != "") {
      $.ajax({
        url: '/items/price_range',
        type: 'GET',
        data: { min: min_price.val(), max: max_price.val()},
        dataType: 'json'
      })
      .done(function(range) {
        if (range) {
          $('#price_range').val(range.id);
        }
      })
      .fail(function() {
        alert('Failed to get the price range')
      })
    }
  }
});

6-3. Remaining tasks

I feel that the description is redundant ... I want to refactor it a little more, so I'm looking for a good way to write it.

7. Clear button for search conditions

7-1. Challenge: Is there no choice but to implement "reset" with js? The description is likely to be redundant

At first, I made a button to initialize (= reset) the form with Rails helper method. Initialize all values in [Rails] form with one click (helper method definition + JavaScript)

However, with this method, the intended movement cannot be performed on the screen after the search conditions have been specified and the search has been executed.

  • Reset is a movement to return to the value (= initial value) when the screen was read.

What this means is that if you perform a search with the keyword "margiela", for example, this screen will appear when you perform the search. スクリーンショット 2020-06-10 11.49.49.png

It is easy to understand that the initial value is the value at the timing when the screen is loaded, so the initial value of the keyword in this state is "margiela". In other words, even if you reset (= initialize the form) in this state, "margiela" in the keyword will not disappear. Since the initial value is "margiela" (rewrite the keyword to "yohji yamamoto" and perform "initialization" to return to "margiela").

That's not what we want to achieve, but the move to return all values to blanks ("select" for pull-downs).

7-2. Countermeasures

Implemented with js. Since it would be redundant if the description runs processing for each item, I decided to search for child elements of the form.

item_search.js


//Operation when the clear button is pressed
$(function () {
  $("#js_conditions_clear").on("click", function () {
      clearForm(this.form);
  });

  function clearForm (form) {
    $(form)
        .find("input, select, textarea")
        .not(":button, :submit, :reset, :hidden")
        .val("")
        .prop("checked", false)
        .prop("selected", false)
    ;
    $('select[name=sort_order]').children().first().attr('selected', true);
    $('#children_category_search').remove();
    $('#grandchildren_category_checkboxes').remove();
  }
});

7-3. Remaining tasks

I feel like there is a better way. .. I will seek.

in conclusion

I don't understand Rails well, so I think there is a more royal way to do it. If there is something like "This way of writing is more beautiful!", I would be grateful if you could let me know.

Recommended Posts