javascriptのみで複数カテゴリの絞り込み検索を実現するコード

はじめに

最終更新2020/08/26 : 検索のコンディションをカンマ区切りで複数持たせられるように改良しました。

こんにちは、北陸のwebコーダーdaimaです。

本日はjavascript(jQuery)のみで 項目の絞り込み検索を実現する サンプルコードをご紹介します。

今回ご紹介するコード(スクリプト)には 主に次のようなメリット、デメリットがあります。

このコードのメリット

  • コピペで簡単&すぐに利用可能
  • 検索件数表示&検索条件リセット機能付き
  • PHPやajax、CMSなどが不要
  • HTMLを触るだけで検索条件の変更が可能(jsに触れなくてもOK!)
  • polyfill無しでChrome、Firefox、safari、IE、スマホ全ての環境で動作可能(記事投稿時点最新版)

このコードのデメリット

  • 全ての検索項目を予め HTML内に書かれておく必要があるため 件数が膨大になると保守が煩雑になる可能性がある。
  • 検索結果表示時のアニメーションなどはなし

デモページとコード

  <div class="bl_3daysSearchBlock">

    <div class="bl_3daysSearchBlock_inner">

      <div class="bl_3daysSearchBlock_ttl">
        <div class="bl_3daysSearchBlock_ttl_main">LUNCH INFO</div>
        <div class="bl_3daysSearchBlock_ttl_sub">県内のランチスポット情報</div>
        
      </div>

      <div id="select" class="bl_selectBlock">

        <div class="el_searchResult">
          <span class="el_searchResult_nume js_numerator"></span>件/全<span class="el_searchResult_deno js_denominator"></span>件
        </div>

        <div class="bl_selectBlock_wrapper">

          <div class="bl_selectBlock_wrapper_wrapper">
            <div class="bl_selectBlock_ttl">ジャンル</div>
            <div class="bl_selectBlock_content js_conditions" data-type="type">
              <span class="bl_selectBlock_check"><input id="type-japan" type="checkbox" name="type" value="japan">
                <label for="type-japan">
                <span class="el_checkbox"></span>
                和食
                </label>
              </span>
              <span class="bl_selectBlock_check"><input id="type-china" type="checkbox" name="type" value="china">
                <label for="type-china">
                <span class="el_checkbox"></span>
                中華
                </label>
              </span>
              <span class="bl_selectBlock_check"><input id="type-italia" type="checkbox" name="type" value="italia">
                <label for="type-italia">
                <span class="el_checkbox"></span>
                イタリアン
                </label>
              </span>
            </div>
          </div>
          <div class="bl_selectBlock_wrapper_wrapper">
            <div class="bl_selectBlock_ttl">価格帯</div>
            <div class="bl_selectBlock_content js_conditions" data-type="price">
              <span class="bl_selectBlock_check"><input id="price-u500" type="checkbox" name="price" value="u500">
                <label for="price-u500">
                  <span class="el_checkbox"></span>
                  〜500円
                </label>
              </span>
              <span class="bl_selectBlock_check"><input id="price-o500u1000" type="checkbox" name="price" value="o500u1000">
                <label for="price-o500u1000">
                  <span class="el_checkbox"></span>
                  501円〜1000円
                </label>
              </span>
              <span class="bl_selectBlock_check"><input id="price-o1000" type="checkbox" name="price" value="o1000">
                <label for="price-o1000">
                  <span class="el_checkbox"></span>
                  1000円〜
                </label>
              </span>
            </div>
          </div>
          <div class="bl_selectBlock_wrapper_wrapper">
            <div class="bl_selectBlock_ttl">場所</div>
            <div class="bl_selectBlock_content js_conditions" data-type="location">
              <span class="bl_selectBlock_check"><input id="location-kanazawa" type="checkbox" name="location" value="kanazawa">
                <label for="location-kanazawa">
                  <span class="el_checkbox"></span>
                  金沢
                </label>
              </span>
              <span class="bl_selectBlock_check"><input id="location-kaga" type="checkbox" name="location" value="kaga">
                <label for="location-kaga">
                  <span class="el_checkbox"></span>
                  加賀
                </label>
              </span>
              <span class="bl_selectBlock_check"><input id="location-komatsu" type="checkbox" name="location" value="komatsu">
                <label for="location-komatsu">
                  <span class="el_checkbox"></span>
                  小松
                </label>
              </span>
            </div>
          </div>
        </div>
        <div class="bl_selectBlock_release js_release">すべての選択を解除</div>
      </div>

      <div class="bl_searchResultBlock">
        <div class="bl_searchResultBlock_item js_target" data-type="japan" data-price="u500" data-location="kanazawa">和食/〜500円/金沢</div>
        <div class="bl_searchResultBlock_item js_target" data-type="china" data-price="u500" data-location="kanazawa">中華/〜500円/金沢</div>
        <div class="bl_searchResultBlock_item js_target" data-type="italia" data-price="u500" data-location="kanazawa">イタリアン/〜500円/金沢</div>
        <div class="bl_searchResultBlock_item js_target" data-type="japan" data-price="o500u1000" data-location="kanazawa">和食/501円〜1000円/金沢</div>
        <div class="bl_searchResultBlock_item js_target" data-type="china" data-price="o500u1000" data-location="kanazawa">中華/501円〜1000円/金沢</div>
        <div class="bl_searchResultBlock_item js_target" data-type="italia" data-price="o500u1000" data-location="kanazawa">イタリアン/501円〜1000円/金沢</div>
        <div class="bl_searchResultBlock_item js_target" data-type="japan" data-price="u500" data-location="kaga" >和食/〜500円/加賀</div>
        <div class="bl_searchResultBlock_item js_target" data-type="china" data-price="u500" data-location="komatsu">中華/〜500円/小松</div>
        <div class="bl_searchResultBlock_item js_target" data-type="italia" data-price="u500" data-location="komatsu">イタリアン/〜500円/小松</div>
        <div class="bl_searchResultBlock_item js_target" data-type="japan" data-price="o1000" data-location="kanazawa">和食/1000円〜/加賀</div>
        <div class="bl_searchResultBlock_item js_target" data-type="japan,china" data-price="o1000" data-location="kanazawa">和食&中華/1000円〜/加賀</div>
      </div>

    </div>

  </div>
    input{
      display: none;
    }
    .bl_3daysSearchBlock{
      margin-bottom: 60px;
    }
    .bl_3daysSearchBlock_inner{
      width: 82.9%;
      max-width: 1160px;
      margin-left: auto;
      margin-right: auto;
    }
    .bl_3daysSearchBlock_ttl{
      text-align: center;
      margin: 55px 0;
    }
    .bl_3daysSearchBlock_ttl_main{
      font-size: 27px;
      color: #0073aa;
      margin-bottom: 15px;
      font-weight: bold;
      letter-spacing: 0.15em;
      font-family: "Josefin Sans" , "NotoSansCJKjp-Jxck", "Noto Sans CJK JP" , "Noto Sans" , "Noto Sans Japanese" ,"Helvetica Neue", "Meiryo" , "メイリオ", "YuGothic", "游ゴシック",sans-serif;
    }
    .bl_3daysSearchBlock_ttl_sub{
      font-size: 16px;
      color: #0073aa;
      letter-spacing: 0.2em;
    }
    .el_searchResult{
      text-align: center;
      color: #0073aa;
    }
    .js_target{
      display: none;
    }
    .js_target.js_selected{
      display: block;
    }
    .bl_selectBlock label{
      display: inline;
    }
    .bl_selectBlock input{
    }
    .bl_selectBlock_check label .el_checkbox{
      position: relative;
      top: 3px;
      width: 14px;
      height: 14px;
      display: inline-block;
      margin-right: 5px;
      border: 1px solid #0073aa;
    }
    .bl_selectBlock_check label .el_checkbox:after{
      content: "";
      position: absolute;
      top: 4px;
      left: 4px;
      bottom: 4px;
      right: 4px;
    }
    .bl_selectBlock_check input[type=checkbox]:checked + label .el_checkbox:after{
      background-color: #0073aa;
    }
    .el_searchResult{
      margin-bottom: 20px;
    }
    .bl_selectBlock{
      border: 1px solid #0073aa;
      padding: 30px;
      box-sizing: border-box;
      margin-bottom: 30px;
      font-size: 14px;
      letter-spacing: 0.1em;
    }
    .bl_selectBlock_wrapper_wrapper{
      display: -webkit-flex;
      display: -moz-flex;
      display: -ms-flex;
      display: -o-flex;
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      margin-bottom: 30px;
    }
    .bl_selectBlock_check label{
      cursor: pointer;
    }
    .bl_selectBlock_ttl{
      width: 185px;
      color: #0073aa;
      letter-spacing: 0.1em;
      margin-bottom: 20px;
    }
    @media screen and (max-width: 765px){
      .bl_selectBlock_ttl{
        width: 100%;
        margin-bottom: 20px;
      }
    }
    .bl_selectBlock_content{
      width: calc(100% - 185px);
    }
    @media screen and (max-width: 765px){
      .bl_selectBlock_content{
        width: 100%;
      }
    }
    .bl_searchResultBlock{
      font-size: 14px;
      border-top: 1px solid #bdbdbd;
    }
    .bl_searchResultBlock_item{
      padding: 20px 0;
      box-sizing: border-box;
      border-bottom: 1px solid #bdbdbd;
    }
    .bl_selectBlock_check{
      display: inline-block;
      margin-right: 25px;
      display: inline-block;
      margin-bottom: 20px;
    }
    .bl_selectBlock_check:last-child{
      margin-right: 0;
    }
    .bl_selectBlock_release{
      text-align: center;
      color: #fff;
      background-color: #bdbdbd;
      padding: 15px 0;
    }
    .js_release{
      cursor: pointer;
    }
  $(function(){
  var box = $('.js_target');//検索対象のDOMを格納する
  var conditions = $('.js_conditions');//現在の条件の選択状況を保持するオブジェクト
  var findConditions;//各data-typeの子要素(input)を格納する
  var currentType;//現在のdata-typeを示す
  var count = 0;//検索ヒット数
  var checkcount = 0;//各data-typeのチェックボックス選択数
  var data_check = 0;//対象項目のデータがどれだけチェック状態と一致しているか
 
  var condition ={};//チェックボックスの入力状態を保持するオブジェクト
 
  $('.js_denominator').text(box.length);//件数表示の分母をセット
 
  for(var i = 0; i < conditions.length; i++){//ターゲットのdata-typeを参照し、メソッドとしてconditionに個別に代入する
    currentType = conditions[i].getAttribute('data-type');
    condition[currentType] = [];
  }
 
  function setConditions(){//条件設定
 
    count = 0;
    box.removeClass('js_selected');
 
    for(var i = 0; i < conditions.length; i++){//data-typeごとの処理
 
      currentType = conditions[i].getAttribute('data-type');
 
      findConditions = conditions[i].querySelectorAll('input');
 
      for(var n = 0; n< findConditions.length; n++){//inputごとの処理
 
        if(findConditions[n].checked){//現在選択中のインプットが選択されている場合
          condition[currentType][findConditions[n].value] = true;
          checkcount++
        } else {
          condition[currentType][findConditions[n].value] = false;
        }
        if(findConditions.length === n+1){//ループが最後の場合
          if(checkcount === 0){
            for(var t = 0; t < findConditions.length; t++){
              condition[currentType][findConditions[t].value] = true;
            }
          }
          checkcount = 0;
        }
      }
    }
 
 
    for(var m = 0, len = box.length; m< len; ++m){//最初に取得したターゲットの情報と、現在のinputの選択状態を比較して処理を行う
 
      for(var i = 0; i < conditions.length; i++){//ターゲットのdata-typeを参照し、メソッドとしてconditionに個別に代入する
        currentType = conditions[i].getAttribute('data-type');
        //現在のターゲットのtype情報をカンマ区切りで分割し、配列に代入
        var currentBoxTypes = $(box[m]).data(currentType).split(',');

        for(var j = 0; j < currentBoxTypes.length; j++){
          if(condition[currentType][currentBoxTypes[j]]){
            data_check++;//選択した条件のうちひとつでもマッチしてたらdata_checkを加算してループを抜ける
            break;
          } else {
   
          }
        }

      }
 
        if(data_check === conditions.length){
          count++;
          $(box[m]).addClass('js_selected');
        }else{
 
        }
        data_check = 0;
 
    }
 
 
    $('.js_numerator').text(count);//件数表示の分子をセット
  }
 
  setConditions();
 
  $(document).on('click','input',function(){
 
    setConditions();
 
  });
 
  $(document).on('click','.js_release',function(){
 
    $('.bl_selectBlock_check input').attr("checked", false);
 
    setConditions();
 
  });
 
});

コードの解説

HTMLは「セレクトボックスエリア」と 「検索結果の表示エリア」の二箇所に分かれています。

セレクトボックスエリア

絞り込みの条件を指定する箇所です。

今回は検索条件が複数必要になる状況を想定して、 検索条件のチェックボックスを囲む 親カテゴリも複数設定できるようにしました。

具体的には、js_conditionsクラスを持つ要素が カテゴリの親となり、その親に指定した data-type属性の値が選択条件のカテゴリ名になります。 (例:職務内容 = job,勤務地 = location)

そして、子要素のinputは 全て親のカテゴリ名がnameになり、 valueがそれぞれの選択肢になります。

ここで設定したカテゴリ名や inputのvalue値は後の 検索結果エリアのHTML内でも使用します。

検索結果エリア

検索結果を表示するエリアです。

この箇所のHTMLを見てみると、 絞り込みを行いたい項目が、 一個ずつjs_targetクラスを指定した タグで囲まれていることが分かります。

そして、そのタグに data-[選択条件のカテゴリ名]の命名規則で 検索ステータスを持たせて 一種の目印としているわけですね。

今回のスクリプトは、 inputボタン(とリセットボタン)のクリック時に 現在のinputの選択状況と 検索対象それぞれの検索ステータスの指定を比較し、 条件に当てはまるものだけに専用のクラスを付与して それ以外のものはdisplay:none;で 表示を隠すという処理をしています。

またひとつの項目に同じ選択条件で 複数のステータスを持たせたい場合(デモの例だと「和食&中華」みたいに)は 「data-type=”japan,china”」のようにカンマ区切りでそれぞれの値を指定してください。

応用編:チェックボックスがひとつも選択されていない場合は検索結果をすべて非表示にするバージョン

  $(function(){
  var box = $('.js_target');//検索対象のDOMを格納する
  var conditions = $('.js_conditions');//現在の条件の選択状況を保持するオブジェクト
  var findConditions;//各data-typeの子要素(input)を格納する
  var findConditionsAll;//全てのinputを格納する
  var currentType;//現在のdata-typeを示す
  var count = 0;//検索ヒット数
  var checkcount = 0;//各data-typeのチェックボックス選択数
  var checktotal = 0;//全体のチェックボックス選択合計数
  var data_check = 0;//対象項目のデータがどれだけチェック状態と一致しているか
 
  var condition ={};//チェックボックスの入力状態を保持するオブジェクト
 
  $('.js_denominator').text(box.length);//件数表示の分母をセット
 
  for(var i = 0; i < conditions.length; i++){//ターゲットのdata-typeを参照し、メソッドとしてconditionに個別に代入する
    currentType = conditions[i].getAttribute('data-type');
    condition[currentType] = [];
  }
 
  function setConditions(){//条件設定
 
    count = 0;
    box.removeClass('js_selected');

    //チェックボックス全体の選択合計数を記憶
    findConditionsAll = document.querySelectorAll('#select input');
    for(var i = 0; i < findConditionsAll.length; i++){
      if(findConditionsAll[i].checked){
        checktotal++;
      }
    }

      for(var i = 0; i < conditions.length; i++){//data-typeごとの処理
   
        currentType = conditions[i].getAttribute('data-type');
   
        findConditions = conditions[i].querySelectorAll('input');
   
        for(var n = 0; n< findConditions.length; n++){//inputごとの処理

          //inputの選択合計数が0でなかったら処理を続行
          if(checktotal !== 0){

            if(findConditions[n].checked){//現在選択中のインプットが選択されている場合
              condition[currentType][findConditions[n].value] = true;
              checkcount++
            } else {
              condition[currentType][findConditions[n].value] = false;
            }
            if(findConditions.length === n+1){//ループが最後の場合
              if(checkcount === 0){
                for(var t = 0; t < findConditions.length; t++){
                  condition[currentType][findConditions[t].value] = true;
                }
              }
              checkcount = 0;
            }

          } else {
            condition[currentType][findConditions[n].value] = false;
          }
   
        }
      }

    checktotal = 0;
 
 
    for(var m = 0, len = box.length; m< len; ++m){//最初に取得したターゲットの情報と、現在のinputの選択状態を比較して処理を行う
 
      for(var i = 0; i < conditions.length; i++){//ターゲットのdata-typeを参照し、メソッドとしてconditionに個別に代入する
        currentType = conditions[i].getAttribute('data-type');
         
        var currentBoxTypes = $(box[m]).data(currentType).split(',');

        for(var j = 0; j < currentBoxTypes.length; j++){
          if(condition[currentType][currentBoxTypes[j]]){
            data_check++;
            break;
          } else {
   
          }
        }
      }
 
        if(data_check === conditions.length){
          count++;
          $(box[m]).addClass('js_selected');
        }else{
 
        }
        data_check = 0;
 
    }
 
 
    $('.js_numerator').text(count);//件数表示の分子をセット
  }
 
  setConditions();
 
  $(document).on('click','input',function(){
 
    setConditions();
 
  });
 
  $(document).on('click','.js_release',function(){
 
    $('.bl_selectBlock_check input').attr("checked", false);
 
    setConditions();
 
  });
 
});

チェックボックスがひとつも選択されていない場合は 検索結果を全て非表示にするバージョンのコードです。 通常版のjsコードと差し替えてご使用ください。

※HTML、CSSは同一です。

応用編2:OR条件にする方法


        if(data_check >= 1){
          count++;
          $(box[m]).addClass('js_selected');
        }else{
 
        }

オリジナルでは検索条件がAND条件(検索条件をすべて満たした項目のみ表示)となっていますが OR条件(検索条件に一つでも合致していれば表示)にしたい場合は javascriptコード82行目の条件分岐個所を上記のように書き換えてご利用ください。

その他補足

実際にご自身のサイト上で 当コードをご利用される場合は 上記の解説を参考にして 選択項目の差し替えや増減を行ってください。

基本的にはスクリプト部分を弄らなくても 検索項目及び検索条件の変更が可能な設計になっています。

さらに補足

https://isotope.metafizzy.co/

より本格的な 絞り込み検索をお求めであれば、 商用利用に条件はありますが、 上記のisotope.jsがおすすめです。

コメント

  1. はま より:

    初めまして、こんばんは

    絞り込み検索を探してほしい機能が揃ったJSでしたので、参考にさせていただいております。
    一点お伺いしたいのですが、セレクトボックスエリアで未選択時に検索結果エリアでは全て表示されておりますが、セレクトボックスエリアで未選択時には検索結果エリア非表示にすることは可能でしょうか。

    お忙しいところ恐れ入りますが、可能でしたらご教示頂けますでしょうか。
    宜しくお願い致します。

    • daima より:

      はまさんコメントありがとうございます。

      記事内に、チェックボックスが一つも選択されていない時に
      検索結果を全て非表示にするバージョンのコードを追記しました。

      ぜひ一度お試しください。

  2. 山中 より:

    検索結果が無かった場合、「条件に当てはまるお店が見つかりませんでした。」と表示させるにはどうしたら良いのでしょうか?

    • daima より:

      お返事遅くなってすみません。

      検索結果が無かった場合、「条件に当てはまるお店が見つかりませんでした。」と表示させるにはどうしたら良いのでしょうか?

      本スクリプトでは
      変数countが検索結果総数を保持しているので、
      setConditions関数の最終行あたりに
      if文でcountが0の時に上記メッセージを
      表示するような処理を追加すればよいかと思います。

  3. ミナト より:

    and検索,or検索を追加することは可能でしょうか。
    例えば、イタリアン,~500円,金沢,小松をチェックした場合
    「イタリアン/〜500円/金沢」
    「イタリアン/〜500円/小松」
    「イタリアン/〜500円/金沢,小松」を表示するです。

  4. blythe より:

    はじめまして、こんにちわ。
    Javascriptの素人です。

    data-type=”japan”

    data-type=”japan china”

    複数にしたい場合はどのように記述すればよろしいでしょうか。

    お忙しいところ恐れ入りますが、可能でしたらご教示頂けますでしょうか。
    よろしくお願いいたします。

    • daima より:

      blytheさん お返事遅くなりすみません。

      当該記事ページのコードを複数条件でも検索可能なように加筆しました。
      お手すきの際にご確認ください。

  5. JJ より:

    こんにちは。記事大変参考になっております。

    1点伺いたいのですが、「 data-type='[“china”, “japan”]’ 」といったような場合、どのように書き換えると良いのでしょうか。
    82行目付近を考えてみたのですが、結局わからずお訊ねした次第です…。

    • daima より:

      JJさん コメントありがとうござます。

      > 1点伺いたいのですが、「 data-type='[“china”, “japan”]’ 」といったような場合、どのように書き換えると良いのでしょうか。

      検索コンディションを複数指定したいという事であれば、本日当該記事をそのようなケースにも対応可能なように加筆修正しましたのでご確認ください(もしご質問の意図が違っていたらお手数ですがまたお知らせください。)