[RUBY] [Rails] La rédaction de notes a créé un formulaire de recherche avancée avec ransack

introduction

Ceci est une description de la création d'un formulaire de recherche avancée comme celui ci-dessous. Ce n'est pas un résumé, mais un avis détaillé.

J'espère que cela aidera ceux qui ont rencontré des problèmes similaires.

56b5aab1bc98bb048821fc50814e8eeb.gif

Formulaire rempli

▼ Trier 201418e4d94c98c62f249943c2713b93.gif

▼ Catégorie f509c4b3dbb2abf3fad585bebb2963c0.gif

▼ Gamme de prix, case à cocher d9de65f9cc351577645bcb4aee008bf9.gif

▼ Bouton Effacer d6353bb4e2e151c601fe06ab613e79ae.gif

Pièce bouchée

  1. problème de requête de ransack
  2. Mise en œuvre du tri des résultats de recherche
  3. Représentation dans la boîte de sélection
  4. Représentation des cases à cocher en vue
  5. Relation entre les objets qui utilisent l'ascendance et le saccage
  6. Implémentation du bouton "Tout" en JavaScript
  7. Mise en place d'un bouton clair pour les conditions de recherche

Préface

Veuillez vous référer à Github et aux articles suivants pour obtenir des explications et l'utilisation de ransack.

Github - ransack Résumé des diverses méthodes de création de formulaires de recherche à l'aide de [Rails] ransack

1. problème de requête de ransack

1-1. Défi: la requête de recherche (@q) doit être placée sur chaque contrôleur

Comme le montre la figure ci-dessus, puisque le formulaire de recherche est placé dans l'en-tête, il est nécessaire de définir la requête (@q) dans before_action sur la plupart des contrôleurs.

Sinon, l'erreur suivante (Aucun objet Ransack :: Search n'a été fourni à search_form_for!) Se produira. スクリーンショット 2020-06-09 18.12.03.png

Il est nécessaire de décrire l'action d'index dans Qiita mentionnée ci-dessus.

items_controller.rb


class ItemsController < ApplicationController
  def index
    @q = Item.ransack(params[:q])
    #Défini ici@Sans q, recherchez_form_L'erreur ci-dessus se produit dans pour
    @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

Au départ, chaque contrôleur a défini une méthode privée et a fait 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

Cependant, avec cette méthode, il est nécessaire de définir une méthode privée de set_item_search_query dans ** tous les contrôleurs qui appellent la page avec le formulaire de recherche (= afficher l'en-tête) **, ce qui viole le principe DRY **. (La description est avant tout gênante).

1-2. Contre-mesures

En conclusion, j'ai défini une méthode dans ʻapplication_controller.rb` et l'ai appelée sur chaque contrôleur requis.

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  #Ajoutez cette description

  def index  
    @items = Item.including.limit(5).desc.trading_not
  end
  
  #La description suivante est supprimée
  # private
  # def set_item_search_query
  #   @q = Student.ransack(params[:q])
  #   @items = @q.result(distinct: true)
  # end

end

1-3. Tâches restantes (plutôt, manque de compréhension)

La méthode set_item_search_query peut être appelée depuis un autre contrôleur indépendamment du fait qu'elle ait été définie au-dessus ou en dessous de private dans ʻapplication_controller.rb`.

Je me demande ce que cela signifie parce que privé sert essentiellement à définir des méthodes que je ne veux pas être appelées par d'autres contrôleurs.

2. Mise en œuvre du tri des résultats de recherche

2-1. Défi: comment réaliser le tri par ordre décroissant des modèles enfants associés

J'ai eu du mal à trier dans l'ordre des plus "favoris!" Associés aux éléments (l'ordre des modèles enfants les plus liés). En dehors de cela, le tri suivant était relativement facile à mettre en œuvre car il trie simplement les colonnes du modèle Item par ordre.

--Prix le plus bas

La vue et le contrôleur sont les suivants. En termes simples, il déclenche un événement lorsque le menu déroulant est modifié en js et transmet la valeur sélectionnée au contrôleur en tant que paramètre de tri.

<détails>

Chaque code (cliquez pour ouvrir) </ summary>

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"}
Trier
    %option{value: "price-asc"}
Commande la moins chère
    %option{value: "price-desc"}
Le prix le plus élevé
    %option{value: "created_at-asc"}
Annonces les plus anciennes
    %option{value: "created_at-desc"}
Dernier ordre d'inscription
    %option{value: "likes-desc"}
préféré!Par ordre décroissant

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


//Comportement de tri
$(function() {
  //L'événement se produit en sélectionnant le menu déroulant
  $('select[name=sort_order]').change(function() {
    //Récupère l'attribut value de la balise d'option sélectionnée
    const sort_order = $(this).val();
    //Branche de destination de transition de page en fonction de la valeur de l'attribut value

    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"; 
    }
    //Page d'affichage actuelle
    let current_html = window.location.href;
    //Fonction de tri en double
    if (location['href'].match(/&sort=*.+/) != null) {
      var remove = location['href'].match(/&sort=*.+/)[0]
      current_html = current_html.replace(remove, '')
    };
    //Transition de page
    window.location.href = current_html + html
  });
  //Comportement après la transition de page
  $(function () {
    if (location['href'].match(/&sort=*.+/) != null) {
      // option[selected: 'selected']Effacer
      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. Contre-mesures

Je pensais que je ne pouvais pas passer le code qui exprime l'ordre du plus grand nombre de modèles enfants aux paramètres passés au contrôleur, alors j'ai décidé de créer une branche conditionnelle côté contrôleur.

items_controller.rb


class ItemsController < ApplicationController
  ~~
  def search
    @q = Item.includes(:images).search(search_params)
    sort = params[:sort] || "created_at DESC"    
    #Le paramètre qui a volé de js"likes_count_desc"Dans le cas de, la description à trier par ordre décroissant des modèles enfants
    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. Tâches restantes

Il semble y avoir une meilleure description ... J'aimerais savoir qui la comprend.

3. Représentation dans la boîte de sélection

3-1 Défi: Comment réaliser des options qui ont un axe différent des propriétés du modèle, comme la «fourchette de prix»?

53b473176e29ae82957ead2f8d843b37.gif

↑ Comme ça, je veux réaliser la fonction que la limite inférieure et la limite supérieure entrent respectivement dans les colonnes lorsque la gamme de prix est sélectionnée. Cependant, la propriété de Item est price (: integer type), donc il semble qu'il ne puisse pas être utilisé.

3-2. Contre-mesures

Créez quelque chose qui peut gérer les données de hachage de la même manière qu'un modèle (ActiveRecord) avec ʻactive_hash`.

Je ne pensais pas "utiliser active_hash!" Depuis le début,

--Require un tableau qui peut être passé à la méthode d'assistance de ʻoptions_from_collection_for_select`

  • De plus, il est nécessaire de changer les valeurs passées en min et max en fonction du contenu sélectionné.
  • Difficulté à écrire ceci en js ――Pouvez-vous utiliser le modèle? Mais c'est compliqué de faire un modèle et même de faire une table

Je pensais avoir utilisé active_hash dans la méthode d'élimination.

Cliquez ici pour les articles qui font référence à active_hash ⇒ active_hash summary

Plus précisément, j'ai créé un modèle appelé «price_range» (qui peut être traité comme) et je l'ai affiché dans la vue.

** ① Créez un fichier manuellement dans app / models et remplissez ce qui suit **

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
  • Le active_hash d'origine est associé au modèle Item, mais cette fois, price_range n'a pas besoin d'être associé au modèle Item, donc la description de appartient_to_active_hash est omise.

** ② Créez des données de tableau avec le fichier de vue et affichez-les avec ʻoptions_from_collection_for_select` **

ruby:search.html.haml


~~
#Critères de recherche de prix
.sc-side__detail__field
  %h5.sc-side__detail__field__label
    %i.fas.fa-search-dollar
    = f.label :price, 'prix'
  .sc-side__detail__field__form
    # PriceRange.tout, actif_Données définies par hachage(hacher)En tant que tableau
    #Il, options_from_collection_for_Développez avec la méthode d'assistance de sélection et de déroulement
    = select_tag :price_range, options_from_collection_for_select(PriceRange.all, :id, :name), { prompt: "Veuillez sélectionner"}
  .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. Tâches restantes

Je pense que ce n'est pas l'usage original de active_hash. .. ..

4. Représentation des cases à cocher en vue

4-1. Défi: comment exprimer des options sans modèle

Parmi les propriétés du modèle Item (produit), les options sont les suivantes. La plupart des options sont active_hash pour créer un modèle (ce qui peut être traité comme).

① Statut du produit (status_id) → modèle avec active_hash? Créer ② Charge des frais d'expédition (delivery_charge_flag) → Décrivez les options directement dans la vue ③ Statut des ventes (trading_status_id) → Modèle avec active_hash? Créer ④ Category (category_id) → categories Il y a un tableau

Tout sauf ① était en difficulté.

4-1-1. ① État du produit (status_id) → Modèle avec active_hash? Créer

Avant les détails de la mission, voici tout d'abord la description de ①. (Cliquez pour ouvrir)

status.rb


# active_est du hachage
class Status < ActiveHash::Base
  self.data = [
      {id: 1, name: 'Nouveau, inutilisé'},
      {id: 2, name: 'Presque inutilisé'},
      {id: 3, name: 'Pas de rayures ou de taches visibles'},
      {id: 4, name: 'Légèrement rayé et sale'},
      {id: 5, name: 'Il y a des rayures et de la saleté'},
      {id: 6, name: 'Mauvais état général'},
  ]
end

item.rb


class Item < ApplicationRecord
~~
  belongs_to_active_hash :status
~~

ruby:search.html.haml


~~
#Conditions de recherche pour l'état du produit
.sc-side__detail__field
  %h5.sc-side__detail__field__label
    %i.fas.fa-star
    = f.label :status_id_eq, 'État du produit'
  .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, 'tout'
    = 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}
~~

Avec cette description, vous pouvez créer un groupe de cases à cocher comprenant "tous". スクリーンショット 2020-06-09 21.25.33.png

Avec cette description, les choix sont affichés sous forme de cases à cocher et les cases à cocher s'affichent telles qu'elles sont à l'écran après la sélection et la recherche. (Parce que la condition de recherche est conservée en tant que paramètre dans l'instance @ q et saute au contrôleur, et que @ q retourné après la recherche contient ce paramètre)

4-1-2. ② Charge des frais d'expédition (delivery_charge_flag) → Décrivez les options directement dans la vue

Le facteur gênant est que dans la forme au moment de la mise en vente de l'article (article / nouveau), la charge des frais d'expédition (que ce soit la charge du vendeur ou celle de l'acheteur) est simplement inscrite dans la vue.

ruby:views/items/_form.html.haml


~~
.field__form.item-area__field__form
  = f.select :delivery_charge_flag, [['frais de port inclus(Fardeau de l'exposant)', '1'], ['Paiement(Charge de l'acheteur)', '2']], include_blank: 'Veuillez sélectionner', required: :true
~~

De plus, puisque le js qui modifie la méthode de livraison est décrit séparément en fonction du drapeau (1 ou 2), si vous le changez de manière significative, la plage d'influence sera grande et une erreur inattendue se produira.

En d'autres termes, il semble que vous ne pouvez pas utiliser active_hash de la même manière que ** ① **.

4-1-3. ③ Statut des ventes (trading_status_id) → modèle avec active_hash? Créer

C'était étonnamment gênant.

Initialement, trading_status a été conçu ci-dessous.

trading_status.rb


class TradingStatus < ActiveHash::Base
  self.data = [
      {id: 1, name: 'Vente'},
      {id: 2, name: 'Pendant le trading'},
      {id: 3, name: 'Vendu'},
      {id: 4, name: 'Brouillon'}
  ]
end

Pour autant que je sache, il semble simple que "en vente" a un identifiant de 1 et "épuisé" a un identifiant de 3, mais ce n'est pas le cas. "Trading (id: 2)" est également inclus dans ** "Epuisé" **. (Pas à vendre)

En d'autres termes, ** le tableau obtenu par active_hash ne peut pas être utilisé comme condition de recherche en l'état **.

Quand j'ai essayé de changer ce statut, je n'ai pas eu le temps de le réparer sans le divulguer car l'influence était large ici aussi. (Le premier design aurait dû être meilleur)

  • ④ La catégorie sera décrite plus tard

4-2. Contre-mesures

Correspondance à ②

C'était étonnamment simple. Lorsque vous définissez des données avec active_hash, si vous utilisez flag au lieu de ʻid`, vous pouvez utiliser active_hash de la même manière que ①.

delivery_charge.rb


class DeliveryCharge < ActiveHash::Base
  self.data = [
      {flag: 1, name: 'frais de port inclus(Fardeau de l'exposant)'},
      {flag: 2, name: 'Paiement(Charge de l'acheteur)'}
  ]
end

Correspondance à ③

C'est la sensation de pousser avec le contrôleur. C'est donc vraiment difficile à comprendre.

Si vous listez ce que vous avez fait

--Sur l'écran des conditions de recherche, créez un tableau (@trading_status) pour afficher les choix sous forme de case à cocher → Ceci est juste une instance de ** juste pour afficher la condition de recherche ** --Une fois que vous effectuez une recherche normale de ransack (pour le moment, "l'état des ventes" n'est pas une condition de recherche) ... ʻA --Branch le traitement à effectuer lorsque "Statut des ventes" est spécifié comme condition de recherche (lorsqueparams.require (: q) [: trading_status_id_in]est vrai) --Redéfinissez@ q` car il est nécessaire de renvoyer le" statut des ventes "spécifié comme condition de recherche à l'écran après la recherche. --Définir et utiliser des méthodes privées pour la redéfinition

  • Le traitement est ramifié lorsque le nombre de "statut de vente" spécifié comme condition de recherche est de un → Lorsqu'il y en a deux, "en vente" et "épuisé" sont spécifiés = "statut de vente" est une condition de recherche Ne sera pas --Lorsque "Épuisé" est spécifié, l'acheteur_id obtient les articles dont la valeur n'est pas nulle ... B
  • Extraire uniquement les éléments communs à ʻA et Bet les définir dans@ items`
  • En d'autres termes, «@ articles» recherché par des conditions de recherche autres que «statut des ventes» est comparé à «articles vendus» qui est «épuisé», et seuls les articles correspondants sont obtenus. ―― Faites la même chose pour "en vente" (= ID_acheteur est nul)

items_controller.rb


~~
def search
  #Définir une instance de tableau pour afficher les conditions de recherche "état des ventes"
  #id 1 et 3=En vente, vendu
  @trading_status = TradingStatus.find [1,3]

  #Une fois, effectuez une recherche normale de ransack (pour le moment, "l'état des ventes" n'est pas une condition de recherche)
  sort = params[:sort] || "created_at DESC"      
  @q = Item.not_draft.search(search_params)
  @items = @q.result(distinct: true).order(sort)
~~
  #Lorsque le statut des ventes est dans les conditions de recherche
  if trading_status_key = params.require(:q)[:trading_status_id_in]
    #Revenir à l'écran après la recherche@Redéfinition de q
    @q = Item.includes(:images).search(search_params_for_trading_status)
    #Une condition de vente spécifiée (si deux sont spécifiées, à la fois en vente et épuisé sont spécifiées = non spécifiées)
    #Et traitement lorsque la clé spécifiée est 3 = "épuisé"
    if trading_status_key.count == 1 && trading_status_key == ["3"]
      #Obtenir des articles épuisés → Dans mon équipe, acheteur dans le tableau des articles_Il a été défini que l'identifiant a une valeur = épuisé
      sold_items = Item.where.not(buyer_id: nil)
      #plus haut@les articles ont été recherchés selon les conditions de recherche reçues par ransack@items
      #Et vendu_Extraire des éléments correspondants avec des éléments → C'est-à-dire&
      @items = @items & sold_items
    elsif trading_status_key.count == 1 && trading_status_key == ["1"]
      #Obtenir des articles à vendre → Dans mon équipe, acheteur dans le tableau des articles_Il a été défini que l'identifiant est nul = en vente
      selling_items = Item.where(buyer_id: nil)
      @items = @items & selling_items
    end
  end

  private
  #Lorsque "Statut des ventes" n'est pas spécifié comme condition de recherche@q → ransack recherche avec cette condition de recherche
  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

  #Lorsque "Statut des ventes" est spécifié comme condition de recherche@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. Tâches restantes

La partie qui est devenue assez compliquée. C'était une scène qui m'a fait réaliser que le premier design était important. Je veux trouver un meilleur moyen que ça!

Sérieux? Problèmes restants

-- @ items n'est pas[] -- @ items = @items & selling_items ou @items = @items & sold_items renvoie [] (= c'est-à-dire qu'il n'y a pas d'article correspondant)

Dans ces conditions, la balle réagit.

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

ʻAVOID (Eviter!) , Il semble donc que le problème N + 1 ne se soit pas réellement produit, mais ... Quand j'ai mâché la phrase en anglais, je me suis demandé si c'était "éviter car un chargement impatient a été détecté là où il n'était pas nécessaire". Cependant, si vous supprimez .includes (: images)comme spécifié, vous obtiendrez une alerte de ʻUSE (use!)Pour d'autres modèles comme vous pouvez vous y attendre.

J'ai abandonné une fois parce que la branche conditionnelle ne fonctionnait pas ici.

5. Relation entre les objets qui utilisent l'ascendance et le saccage

5-1. Défi: comment envoyer une catégorie au contrôleur comme condition de recherche

Les catégories sont divisées en parents, enfants et petits-enfants. La table items n'a que category_id, et le category_id sélectionné ici est l'id de la catégorie petit-enfant.

D'autre part, en tant que méthode de recherche, il est extrêmement pratique de pouvoir effectuer une recherche par "lorsque seule la catégorie parente est sélectionnée" ou "lorsque seule la catégorie enfant est sélectionnée". Comment y parvenir? C'est un problème.

Si vous subdivisez davantage les problèmes

① Je veux faire de la catégorie petit-enfant une case à cocher au lieu d'un menu déroulant (2) Les conditions "lorsque seule la catégorie parente est sélectionnée" et "lorsque la catégorie enfant est sélectionnée" ne peuvent pas être renvoyées correctement dans le champ des conditions de recherche après la recherche. ③ Si vous créez un formulaire qui ne reçoit que «category_id_in», vous ne pouvez pas effectuer de recherche par catégorie parent / catégorie enfant. ④ Si vous soumettez le formulaire avec uniquement la catégorie parent sélectionnée, les paramètres seront ignorés avec category_id vide.

Beaucoup ...

5-1-1. ① Je veux faire de la catégorie petit-enfant une case à cocher au lieu d'un menu déroulant

Une seule catégorie de petits-enfants peut être sélectionnée (menu déroulant), pas ↓ c2a4191736e7b580941b142cb934587d.gif

Je veux pouvoir sélectionner plusieurs catégories de petits-enfants (case à cocher) ↓ d97b6e77fc3888c783c49f51b73c1304.gif

5-1-2. (2) Les conditions "lorsque seule la catégorie parente est sélectionnée" et "lorsque la catégorie enfant est sélectionnée" ne peuvent pas être renvoyées dans le champ de condition de recherche après la recherche.

Par exemple, si vous spécifiez et recherchez comme ceci, スクリーンショット 2020-06-09 23.54.22.png ↓ Après la recherche, le champ de condition sera dans un état où rien n'est spécifié (la même chose s'applique lorsque même des catégories enfants sont sélectionnées). スクリーンショット 2020-06-09 23.55.18.png

5-1-3. ③ Si vous créez un formulaire qui ne reçoit que category_id_in, vous ne pouvez pas effectuer de recherche par catégorie parente / catégorie enfant.

La description de la première vue était comme celle-ci, mais dans ce cas, la catégorie ne fonctionnait pas comme condition de recherche à moins que la catégorie de petit-enfant ne soit sélectionnée.

ruby:search.html.haml


%li
  = f.select :category_id_in ,options_for_select(@parents, @item.category.root.name),{include_blank: "Veuillez sélectionner"}, {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: "Veuillez sélectionner"}, {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: "Veuillez sélectionner"}, {id: 'grandchildren_category', required: :true}

Seule la catégorie petit-enfant peut être recherchée par le category_id_in reçu par le paramètre, donc même si seule la catégorie parent est sélectionnée dans cet état, il n'y a pas de produit correspondant. (Même si vous sélectionnez uniquement "Hommes", vous ne pouvez pas rechercher de produits dont la catégorie parente est Hommes.)

5-1-4. ④ Si vous soumettez le formulaire avec uniquement la catégorie parent sélectionnée, les paramètres seront ignorés avec category_id vide.

En guise de contre-mesure pour (3), les propriétés de la catégorie parent et de la catégorie enfant ont été définies sur category_id (la raison sera décrite plus tard), mais si la recherche est exécutée avec la catégorie enfant non sélectionnée, category_id sera ignoré dans un état vide.

5-2. Contre-mesures

Poussez avec js et contrôleur.

5-2-1. (1) Correspondance pour vouloir faire de la catégorie petit-enfant une case à cocher au lieu de dérouler

Résolvez la vue et js comme suit.

Cliquez pour voir le code

ruby:search.html.haml


.sc-side__detail__field
  %h5.sc-side__detail__field__label
    %i.fas.fa-list-ul
    = f.label :category_id, 'Choisir une catégorie'
  .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: "Veuillez sélectionner"}, {id: 'parent_category_search'}
        %li
          - if @category_child.present?
            = f.select :category_id, options_for_select(@category_child_array, @category_child.id),{include_blank: "Veuillez sélectionner"}, {id: 'children_category_search'}
          - else
            = f.select :category_id, @category_child_array, {include_blank: "Veuillez sélectionner"}, {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, 'tout'
            = 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: "Veuillez sélectionner"}, {id: 'parent_category_search'}

item_search.js


  //Comportement des catégories dans le formulaire de recherche
  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;
  }

  //Créer un affichage des catégories enfants
  function appendChildrenBox(insertHTML){
    const childSelectHtml = `<li>
                              <select id="children_category_search" name="q[category_id]">
                                <option value="">Veuillez sélectionner</option>
                                ${insertHTML}
                              </select>
                            </li>`;
    $('.field__input--category_search').append(childSelectHtml);
  }
  //Créer un affichage de la catégorie des petits-enfants
  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">tout</label>
                                    </div>
                                  </div>
                                  ${insertHTML}
                                </li>`;
    $('.field__input--category_search').append(grandchildSelectHtml);
  }
  //Événements après sélection de la catégorie parente
  $('#parent_category_search').on('change', function(){
    //Obtenez le nom de la catégorie parent sélectionnée
    const parentName =$(this).val(); 
    if (parentName != ""){ 
      //Assurez-vous que la catégorie parente n'est pas la catégorie par défaut
      $.ajax({
        url: '/items/category_children',
        type: 'GET',
        data: { parent_name: parentName },
        dataType: 'json'
      })
      .done(function(children){
         //Lorsque le parent change, supprimez les enfants et ci-dessous
        $('#children_category_search').remove();
        $('#grandchildren_category_checkboxes').remove();
        let insertHTML = '';
        children.forEach(function(child){
          insertHTML += appendOption(child);
        });
        appendChildrenBox(insertHTML);
      })
      .fail(function(){
        alert('Échec de l'obtention de la catégorie');
      })
    }else{
      //Lorsque la catégorie parent devient la valeur initiale, supprimez les enfants et ci-dessous
      $('#children_category_search').remove();
      $('#grandchildren_category_checkboxes').remove();
    }
  });
  //Événement après sélection de la catégorie enfant
  $('.field__input--category_search').on('change', '#children_category_search', function(){
    const childId = $(this).val();
    //Obtenir l'identifiant de la catégorie enfant sélectionnée
    if (childId != ""){ 
      //Assurez-vous que la catégorie enfant n'est pas la catégorie par défaut
      $.ajax({
        url: '/items/category_grandchildren',
        type: 'GET',
        data: { child_id:  childId},
        dataType: 'json'
      })
      .done(function(grandchildren){
        if (grandchildren.length != 0) {
          //Lorsqu'un enfant est changé, supprimez le petit-enfant et les dessous
          $('#grandchildren_category_checkboxes').remove();
          let insertHTML = '';
          grandchildren.forEach(function(grandchild){
            insertHTML += appendCheckbox(grandchild);
          });
          appendGrandchildrenBox(insertHTML);
        }
      })
      .fail(function(){
        alert('Échec de l'obtention de la catégorie');
      })
    }else{
      $('#grandchildren_category_checkboxes').remove();
    }
  });

5-2-2. Correspondance avec ②③

Définissez le nom de propriété du formulaire de la catégorie parent et de la catégorie enfant sur category_id et traitez-le avec le contrôleur. La vue est au dessus.

Cliquez ici pour le contrôleur

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: "Liste des catégories").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
    #Lorsque le statut des ventes est dans les conditions de recherche
    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

    #Lorsque la catégorie est dans les critères de recherche
    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?
          #Catégorie Parentale
          @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?("/")
          #Catégorie enfant
          @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
          #Ramassez la catégorie des petits-enfants avec saccage → catégorie_id_in
      end
    end
    @items = Kaminari.paginate_array(@items).page(params[:page]).per(20)
  end

5-2-3. ④ Lors de la soumission d'un formulaire avec uniquement la catégorie parent sélectionnée, la réponse au paramètre saute lorsque category_id est vide

Dans js, lorsque la catégorie enfant est vide (non sélectionnée) lors de l'exécution de la recherche, cela correspond au processus de suppression du menu déroulant de la catégorie enfant

item_search.js


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

5-3. Tâches restantes

Comme 4-3, la balle réagit parfois ...

De plus, si vous exécutez une recherche (= soumettre un formulaire) avec uniquement la catégorie parent sélectionnée (lorsque la catégorie enfant n'est pas sélectionnée), le paramètre category_id sera passé au contrôleur vide. Cette fois, j'ai pris la méthode de suppression de la catégorie enfant f.select avec js, mais il semble y avoir un meilleur moyen.

6. Implémentation du bouton "Tout" en JavaScript

6-1. Défi: réalisation de "tout" compliqué de manière inattendue

--Lorsque vous appuyez sur "Tout", sélectionnez toutes les options correspondantes. ――Si même l'une des options applicables n'est pas cochée, supprimez également «Tout».

  • Cochez "Tout" lorsque toutes les options autres que "Tout" sont sélectionnées. --Sur l'écran après la recherche, si toutes les options autres que "Tout" sont sélectionnées, cochez également "Tout".

J'étais accro à la façon de réaliser l'exigence d'une complexité inattendue. ↓ Comportement comme celui-ci 836dea5b6925b9d31ed861810c3352d6.gif

6-2. Contre-mesures

Implémenté avec js.

Il existe trois principaux types de traitement. ** ① Comportement lorsque vous cliquez sur "Tout" ** ** ② Comportement lorsque vous cliquez sur autre que "Tous" ** ** ③ Fonction pour juger s'il faut cocher la case "Tout" lors du chargement de la page **

Si vous n'écrivez pas le processus de ③, exécutez la recherche avec toutes les options sélectionnées dans les conditions de recherche → La case à cocher "Tous" sera décochée dans la colonne des conditions après la recherche, ce qui est gênant.

Cliquez ici pour le code

. </ résumé>

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][]"]')

  //① Comportement en cliquant sur "Tout"
  $(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));
  });

  //② Comportement lors d'un clic autre que "Tout"
  $(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));
  });


  //Une fonction qui détermine s'il faut cocher la case "Tout" lors du chargement d'une 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);
    }
  }

  //③ Lorsque la page est chargée, exécutez une fonction qui détermine s'il faut cocher la case "Tout".
  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('Impossible d'obtenir la fourchette de prix')
      })
    }
  }
});

6-3. Tâches restantes

J'ai l'impression que la description est redondante ... Je veux la refactoriser un peu plus, donc je cherche une bonne façon de l'écrire.

7. Bouton Effacer pour les conditions de recherche

7-1 Défi: N'y a-t-il pas d'autre choix que d'implémenter "reset" dans js? La description est susceptible d'être redondante

Au début, j'ai créé un bouton pour initialiser (= réinitialiser) le formulaire avec la méthode d'assistance de Rails. Initialisez toutes les valeurs sous la forme [Rails] en un seul clic (définition de méthode d'aide + JavaScript)

Cependant, avec cette méthode, le mouvement prévu ne peut pas être effectué sur l'écran une fois que les conditions de recherche ont été spécifiées et que la recherche a été exécutée.

  • La réinitialisation est un mouvement pour revenir à la valeur (= valeur initiale) lors de la lecture de l'écran.

Cela signifie que si vous effectuez une recherche avec le mot-clé "margiela", par exemple, cet écran apparaîtra lorsque vous exécuterez la recherche. スクリーンショット 2020-06-10 11.49.49.png

Il est facile de comprendre que la valeur initiale est la valeur au moment où l'écran est chargé, donc la valeur initiale du mot-clé dans cet état est "margiela". En d'autres termes, même si vous réinitialisez (= initialisez le formulaire) dans cet état, "margiela" dans le mot-clé ne disparaîtra pas. Puisque la valeur initiale est "margiela" (réécrivez le mot-clé en "yohji yamamoto" et effectuez une "initialisation" pour revenir à "margiela").

Ce n'est pas ce que nous voulons réaliser, mais le mouvement de renvoyer toutes les valeurs aux blancs ("sélectionner" pour les menus déroulants).

7-2. Contre-mesures

Implémenté avec js. Puisqu'il serait redondant si la description exécute le traitement pour chaque élément, j'ai décidé de rechercher des éléments enfants du formulaire.

item_search.js


//Fonctionnement lorsque le bouton d'effacement est enfoncé
$(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. Tâches restantes

J'ai l'impression qu'il y a une meilleure façon. .. Je chercherai.

en conclusion

Je ne comprends pas bien Rails, donc je pense qu'il y a une façon plus royale de le faire. S'il y a quelque chose comme "Cette façon d'écrire est plus belle!", Je vous serais reconnaissant de bien vouloir me le faire savoir.

Recommended Posts